//C- -------------------------------------------------------------------
//C- Java DjVu (r) (v. 0.8)
//C- Copyright (c) 2004-2005 LizardTech, Inc.  All Rights Reserved.
//C- Java DjVu is protected by U.S. Pat. No.C- 6,058,214 and patents
//C- pending.
//C-
//C- This software is subject to, and may be distributed under, the
//C- GNU General Public License, Version 2. The license should have
//C- accompanied the software or you may obtain a copy of the license
//C- from the Free Software Foundation at http://www.fsf.org .
//C-
//C- The computer code originally released by LizardTech under this
//C- license and unmodified by other parties is deemed "the LIZARDTECH
//C- ORIGINAL CODE."  Subject to any third party intellectual property
//C- claims, LizardTech grants recipient a worldwide, royalty-free,
//C- non-exclusive license to make, use, sell, or otherwise dispose of
//C- the LIZARDTECH ORIGINAL CODE or of programs derived from the
//C- LIZARDTECH ORIGINAL CODE in compliance with the terms of the GNU
//C- General Public License.   This grant only confers the right to
//C- infringe patent claims underlying the LIZARDTECH ORIGINAL CODE to
//C- the extent such infringement is reasonably necessary to enable
//C- recipient to make, have made, practice, sell, or otherwise dispose
//C- of the LIZARDTECH ORIGINAL CODE (or portions thereof) and not to
//C- any greater extent that may be necessary to utilize further
//C- modifications or combinations.
//C-
//C- The LIZARDTECH ORIGINAL CODE is provided "AS IS" WITHOUT WARRANTY
//C- OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
//C- TO ANY WARRANTY OF NON-INFRINGEMENT, OR ANY IMPLIED WARRANTY OF
//C- MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
//C-
//C- In addition, as a special exception, LizardTech Inc. gives permission
//C- to link the code of this program with the proprietary Java
//C- implementation provided by Sun (or other vendors as well), and
//C- distribute linked combinations including the two. You must obey the
//C- GNU General Public License in all respects for all of the code used
//C- other than the proprietary Java implementation. If you modify this
//C- file, you may extend this exception to your version of the file, but
//C- you are not obligated to do so. If you do not wish to do so, delete
//C- this exception statement from your version.
//C- -------------------------------------------------------------------
//C- Developed by Bill C. Riemers, Foxtrot Technologies Inc. as work for
//C- hire under US copyright laws.
//C- -------------------------------------------------------------------
//
package com.lizardtech.djvu{
//import java.beans.*;
import java.io.*;
import java.util.*;


/**
 * The GPixmapScaler class provides a method to rescale portions of GPixmap
 * images.
 *
 * @author $author$
 * @version $Revision: 1.4 $
 */
public class GPixmapScaler
{
  //~ Static fields/initializers ---------------------------------------------

  private static var FRACBITS:int= 4;
  private static var FRACSIZE:int= (1<< FRACBITS);
  private static var FRACSIZE2:int= (FRACSIZE >> 1);
  private static var FRACMASK:int= (FRACSIZE - 1);
  
  // create array of array
  //private static final short[][] interp    = new short[FRACSIZE][512];
  private static var interp:Array = new Array(FRACSIZE);
  
  initStatic();
  
  static function initStatic() {
	  for(var a:int=0; a<FRACSIZE; a++) {
	  	interp[a] = new Array(512);
	  }
	  
	    for(var i:int= 0; i < FRACSIZE; i++)
	    {
	      var deltas:Array= interp[i];
	
	      for(var j:int= -255; j <= 255; j++)
	      {
	        deltas[256+ j] = int((((j * i) + FRACSIZE2) >> FRACBITS));
	      }
	    }
  }

  //~ Instance fields --------------------------------------------------------

  private var p1:GPixmap= null;
  private var p2:GPixmap= null;
  private var hcoord:Array= null;

  // Fixed point coordinates
  private var vcoord:Array= null;
  private var destHeight:int= 0;
  private var destWidth:int= 0;

  // Temporaries
  private var l1:int= (-1);
  private var l2:int= (-1);
  private var redh:int= 0;
  private var redw:int= 0;
  private var srcHeight:int= 0;

  // The sizes
  private var srcWidth:int= 0;
  private var xshift:int= 0;
  private var yshift:int= 0;

  //~ Constructors -----------------------------------------------------------


  /**
   * Creates a new GPixmapScaler object.
   *
   * @param srcWidth width of the source image
   * @param srcHeight height of the source image
   * @param destWidth width of the target image
   * @param destHeight height of the target image
   */
  public function GPixmapScaler(
    srcWidth:int=0,
    srcHeight:int=0,
    destWidth:int=0,
    destHeight:int=0)
  {
  	if(srcWidth || destWidth) { 
    	setSrcSize(srcWidth, srcHeight);
    	setDestSize(destWidth, destHeight);
    }
  }

  //~ Methods ----------------------------------------------------------------

  /**
   * Sets the size of the output image.  This size is used to initialize the
   * internal data structures of the scaler object.
   *
   * @param destWidth width of the destination image map.
   * @param destHeight height of the destination image map.
   */
  final function setDestSize(
    destWidth:int,
    destHeight:int):void{
    this.destWidth    = destWidth;
    this.destHeight   = destHeight;
    vcoord            = null;
    hcoord            = null;
  }

  /**
   * Sets the horizontal scaling ratio #numer/denom#.  This function may be
   * used to force an exact scaling ratio.  The scaling ratios are otherwise
   * derived from the sizes of the input and output images.
   *
   * @param numer upsample rate
   * @param denom subsample rate
   *
   * @throws IllegalStateException if an error occurs
   * @throws IllegalArgumentException if an error occurs
   */
  final function setHorzRatio(
    numer:int,
    denom:int):void{
    if(
      (srcWidth <= 0)
      || (srcHeight <= 0)
      || (destWidth <= 0)
      || (destHeight <= 0))
    {
      throw new Error("Scaler undefined size");
    }

    // Implicit ratio (determined by the input/output sizes)
    if((numer == 0) && (denom == 0))
    {
      numer   = destWidth;
      denom   = srcWidth;
    }
    else if((numer <= 0) || (denom <= 0))
    {
      throw new IllegalArgumentException("Scaler illegal ratio");
    }

    // Compute horz reduction
    xshift   = 0;
    redw     = srcWidth;

    while((numer + numer) < denom)
    {
      xshift++;
      redw = (redw + 1) >> 1;
      numer <<= 1;
    }

    // Compute coordinate table
    if(hcoord == null)
    {
      hcoord = new int[destWidth];
    }

    prepare_coord(hcoord, redw, destWidth, denom, numer);
  }

  /**
   * Computes which input pixels are required to compute specified output
   * pixels.  Let us assume that we only need a part of the output image.
   * This part is defined by rectangle targetRect.  Only a part of the input
   * image is necessary to compute the output pixels.  This  method computes
   * the coordinates of that part of the input image, and stores them into
   * rectangle sourceRect.
   *
   * @param targetRect Bounding rectangle for the target output.
   *
   * @return Bounding rectangle of portion of the input rectangle used.
   */
  final function getRequiredRect(targetRect:GRect):GRect{
    var red:GRect= new GRect();

    return createRectangles(targetRect, red);
  }

  /**
   * Sets the size of the input image.  This size is used to initialize the
   * internal data structures of the  scaler object.
   *
   * @param srcWidth width of the source image map.
   * @param srcHeight height of the source image map.
   */
  final function setSrcSize(
    srcWidth:int,
    srcHeight:int):void{
    this.srcWidth    = srcWidth;
    this.srcHeight   = srcHeight;
    vcoord           = null;
    hcoord           = null;
  }

  /**
   * Sets the vertical scaling ratio to #numer/denom#.  This function may be
   * used to force an exact scaling ratio.  The scaling ratios are otherwise
   * derived from the sizes of the input and output images.
   *
   * @param numer numerator
   * @param denom denominator
   *
   * @throws IllegalStateException if an error occurs
   * @throws IllegalArgumentException if an error occurs
   */
  final function setVertRatio(
    numer:int,
    denom:int):void{
    if(
      (srcWidth <= 0)
      || (srcHeight <= 0)
      || (destWidth <= 0)
      || (destHeight <= 0))
    {
      throw new Error("Scaler undefined size");
    }

    // Implicit ratio (determined by the input/output sizes)
    if((numer == 0) && (denom == 0))
    {
      numer   = destHeight;
      denom   = srcHeight;
    }
    else if((numer <= 0) || (denom <= 0))
    {
      throw new IllegalArgumentException("Scaler illegal ratio");
    }

    // Compute horz reduction
    yshift   = 0;
    redh     = srcHeight;

    while((numer + numer) < denom)
    {
      yshift++;
      redh = (redh + 1) >> 1;
      numer <<= 1;
    }

    // Compute coordinate table
    if(vcoord == null)
    {
      vcoord = new int[destHeight];
    }

    prepare_coord(vcoord, redh, destHeight, denom, numer);
  }

  // Helper
  final function createRectangles(
    desired:GRect,
    red:GRect):GRect{
    var inp:GRect= new GRect();

    // Parameter validation
    if(
      (desired.xmin < 0)
      || (desired.ymin < 0)
      || (desired.xmax > destWidth)
      || (desired.ymax > destHeight))
    {
      throw new IllegalArgumentException(
        "desired rectangle too big: " + desired.xmin + "," + desired.ymin
        + "," + desired.xmax + "," + desired.ymax + "," + destWidth + ","
        + destHeight);
    }

    // Compute ratio (if not done yet)
    if(vcoord == null)
    {
      setVertRatio(0, 0);
    }

    if(hcoord == null)
    {
      setHorzRatio(0, 0);
    }

    // Compute reduced bounds
    red.xmin   = (hcoord[desired.xmin]) >> FRACBITS;
    red.ymin   = (vcoord[desired.ymin]) >> FRACBITS;
    red.xmax   = ((hcoord[desired.xmax - 1] + FRACSIZE) - 1) >> FRACBITS;
    red.ymax   = ((vcoord[desired.ymax - 1] + FRACSIZE) - 1) >> FRACBITS;

    // Borders
    red.xmin   = (red.xmin > 0)
      ? red.xmin
      : 0;
    red.xmax   = (red.xmax < redw)
      ? (red.xmax + 1)
      : redw;
    red.ymin   = (red.ymin > 0)
      ? red.ymin
      : 0;
    red.ymax   = (red.ymax < redh)
      ? (red.ymax + 1)
      : redh;

    // Input
    inp.xmin = red.xmin << xshift;

    if(inp.xmin < 0)
    {
      inp.xmin = 0;
    }

    inp.xmax = red.xmax << xshift;

    if(inp.xmax > srcWidth)
    {
      inp.xmax = srcWidth;
    }

    inp.ymin = red.ymin << yshift;

    if(inp.ymin < 0)
    {
      inp.ymin = 0;
    }

    inp.ymax = red.ymax << yshift;

    if(inp.ymax > srcHeight)
    {
      inp.ymax = srcHeight;
    }

    return inp;
  }

  /**
   * Computes a segment of the rescaled output image.  The pixmap #output# is
   * overwritten with the segment of the output image specified by the
   * rectangle #targetRect#.  The rectangle #provided_input# specifies which
   * segment of the sourceMap image is provided in the pixmap #input#. An
   * exception \Ref{GException} is thrown if the rectangle #provided_input#
   * is smaller then the rectangle #sourceRect# returned by function
   * \Ref{GPixmapScaler::getSourceRect}.
   *
   * @param provided_input input rectangle
   * @param sourceMap input image map
   * @param targetRect target rectangle
   * @param output target image map
   *
   * @throws IllegalArgumentException DOCUMENT ME!
   * @throws IllegalStateException DOCUMENT ME!
   */
  final function scale(
    provided_input:GRect,
    sourceMap:GPixmap,
    targetRect:GRect,
    output:GPixmap):void{
    // Compute rectangles
    var required_red:GRect= new GRect();
    var sourceRect:GRect= createRectangles(targetRect, required_red);

    // Parameter validation
    if(
      (provided_input.width() != sourceMap.columns())
      || (provided_input.height() != sourceMap.rows()))
    {
      throw new IllegalArgumentException("invalid rectangle");
    }

    if(
      (provided_input.xmin > sourceRect.xmin)
      || (provided_input.ymin > sourceRect.ymin)
      || (provided_input.xmax < sourceRect.xmax)
      || (provided_input.ymax < sourceRect.ymax))
    {
      throw new Error("invalid rectangle");
    }

    // Adjust output pixmap
    if(
      (targetRect.width() != int(output.columns()))
      || (targetRect.height() != int(output.rows())))
    {
      output.init(
        targetRect.height(),
        targetRect.width(),
        null);
    }

    // Prepare temp stuff 
    var bufw:int= required_red.width();
    var lbuffer:Array= new GPixel[bufw + 2];

    for(var i:int= 0; i < lbuffer.length;)
    {
      lbuffer[i++] = new GPixel();
    }

    try
    {
      if((xshift > 0) || (yshift > 0))
      {
        p1   = GPixmap.createGPixmap(output).init(1, bufw, null);
        p2   = GPixmap.createGPixmap(output).init(2, bufw, null);
        l1   = l2 = -1;
      }

      // Loop on output lines
      for(var y:int= targetRect.ymin; y < targetRect.ymax; y++)
      {
        // Perform vertical interpolation
        {
          var fy:int= vcoord[y];
          var fy1:int= fy >> FRACBITS;
          var fy2:int= fy1 + 1;
          var upper:GPixelReference;
          var lower:GPixelReference;

          // Obtain upper and lower line in reduced image
          if((xshift > 0) || (yshift > 0))
          {
            lower   = get_line(fy1, required_red, provided_input, sourceMap);
            upper   = get_line(fy2, required_red, provided_input, sourceMap);
          }
          else
          {
            var dx:int= required_red.xmin - provided_input.xmin;

            if(required_red.ymin > fy1)
            {
              fy1 = required_red.ymin;
            }

            if(required_red.ymax <= fy2)
            {
              fy2 = required_red.ymax - 1;
            }

            lower =
              sourceMap.createGPixelReference(fy1 - provided_input.ymin, dx);
            upper =
              sourceMap.createGPixelReference(fy2 - provided_input.ymin, dx);
          }

          // Compute line
          var idest:int= 1;
          var deltas:Array= interp[fy & FRACMASK];

          for(
            var edest:int= idest + bufw;
            idest < edest;
            upper.incOffset(), lower.incOffset())
          {
            var zdest:GPixel= lbuffer[idest++];
            var lower_r:int= lower.getRed();
            var delta_r:int= deltas[(256+ upper.getRed()) - lower_r];
            var lower_g:int= lower.getGreen();
            var delta_g:int= deltas[(256+ upper.getGreen()) - lower_g];
            var lower_b:int= lower.getBlue();
            var delta_b:int= deltas[(256+ upper.getBlue()) - lower_b];
            zdest.setBGR(lower_b + delta_b, lower_g + delta_g, lower_r + delta_r);
          }
        }

        // Perform horizontal interpolation
        {
          // Prepare for side effects
          lbuffer[0] = lbuffer[1];

          // lbuffer[bufw] = lbuffer[bufw];
          var line:int= 1- required_red.xmin;
          var dest:GPixelReference=
            output.createGPixelReference(y - targetRect.ymin, 0);

          // Loop horizontally
          for(var x:int= targetRect.xmin; x < targetRect.xmax; x++)
          {
            var n:int= hcoord[x];
            var zlower:int= line + (n >> FRACBITS);
            var lower0:GPixel= lbuffer[zlower];
            var lower1:GPixel= lbuffer[zlower + 1];
            var deltas:Array= interp[n & FRACMASK];
            var lower_r:int= lower0.getRed();
            var delta_r:int= deltas[(256+ lower1.getRed()) - lower_r];
            var lower_g:int= lower0.getGreen();
            var delta_g:int= deltas[(256+ lower1.getGreen()) - lower_g];
            var lower_b:int= lower0.getBlue();
            var delta_b:int= deltas[(256+ lower1.getBlue()) - lower_b];
            dest.setBGR(lower_b + delta_b, lower_g + delta_g, lower_r + delta_r);
            dest.incOffset();
          }
        }
      }
    }
    finally
    {
      p1   = null;
      p2   = null;
    }
  }

  private static function prepare_coord(
    coord:Array,
    inmax:int,
    outmax:int,
    inp:int,
    out:int):void{
    var len:int= (inp * FRACSIZE);
    var beg:int= ((len + out) / (2* out)) - FRACSIZE2;

    // Bresenham algorithm
    var y:int= beg;
    var z:int= out / 2;
    var inmaxlim:int= (inmax - 1) * FRACSIZE;

    for(var x:int= 0; x < outmax; x++)
    {
      coord[x]   = (y < inmaxlim)
        ? y
        : inmaxlim;
      z   = z + len;
      y   = y + (z / out);
      z   = z % out;
    }

    // Result must fit exactly
    if((out == outmax) && (y != (beg + len)))
    {
      throw new Error("Scaler assertion");
    }
  }

  // Helpers
  private function get_line(
    fy:int,
    required_red:GRect,
    provided_input:GRect,
    input:GPixmap):GPixelReference{
    if(fy < required_red.ymin)
    {
      fy = required_red.ymin;
    }
    else if(fy >= required_red.ymax)
    {
      fy = required_red.ymax - 1;
    }

    // Cached line
    if(fy == l2)
    {
      return p2.createGPixelReference(0);
    }

    if(fy == l1)
    {
      return p1.createGPixelReference(0);
    }

    // Shift
    var p:GPixmap= p1;
    p1   = p2;
    l1   = l2;
    p2   = p;
    l2   = fy;

    // Compute location of line
    var line:GRect= new GRect();
    line.xmin   = required_red.xmin << xshift;
    line.xmax   = required_red.xmax << xshift;
    line.ymin   = fy << yshift;
    line.ymax   = (fy + 1) << yshift;
    line.intersect(line, provided_input);
    line.translate(-provided_input.xmin, -provided_input.ymin);

    // Prepare variables
    var botline:int= input.rowOffset(line.ymin);
    var rowsize:int= input.getRowSize();
    var sw:int= 1<< xshift;
    var div:int= xshift + yshift;
    var rnd:int= 1<< (div - 1);
    var rnd2:int= rnd + rnd;

    var inp1:GPixelReference= input.createGPixelReference(0);
    var ip:GPixelReference= p.createGPixelReference(0);

    // Compute averages
    for(var x:int= line.xmin; x < line.xmax; x += sw, ip.incOffset())
    {
      var r:int= 0;
      var g:int= 0;
      var b:int= 0;
      var s:int= 0;
      var inp0:int= botline + x;
      var sy2:int= line.height();
      var sy1:int= (1<< yshift);

      if(sy1 > sy2)
      {
        sy1 = sy2;
      }

      for(var sy:int= 0; sy < sy1; sy++, inp0 += rowsize)
      {
        var sx1:int= x + sw;
        inp1.setOffset(inp0);

        if(sx1 > line.xmax)
        {
          sx1 = line.xmax;
        }

        for(var sx:int= sx1 - x; sx-- > 0; s++, inp1.incOffset())
        {
          r += inp1.getRed();
          g += inp1.getGreen();
          b += inp1.getBlue();
        }
      }

      if(s == rnd2)
      {
        ip.setBGR((b + rnd) >> div, (g + r) >> div, (r + rnd) >> div);
      }
      else
      {
        ip.setBGR((b + (s / 2)) / 2, (g + (s / 2)) / s, (r + (s / 2)) / s);
      }
    }

    // Return
    return p2.createGPixelReference(0);
  }
}}